/**
 **oj/ji - A vi like code editor
 *
 * This module provides a vi like code editor for editing oj files.
 *
 * Ji has two modes, insert mode and command mode. The editor begins in
 * insert mode, indicated by a thin caret positioned between characters.
 * To switch from insert mode to command mode press [ESC]. Command mode
 * is indicated by a wide caret and the visibility of the command bar.
 *
 **Command mode
 *
 * Switching back to insert mode
 *
 *.a    append text, after the cursor
 *.A    append text, after end of line
 *.i    insert text, before the cursor
 *.I    insert text, before first non-whitespace character
 *.o    open new line below cursor in Insert Mode
 *.O    open new line above cursor in Insert Mode  
 *
 **oj.main
 *
 * The initial open file can be set with the parameter f.
 *
 *. URL:     /oj/ji?f=/file.js
 *. oj/term: oj/ji file.js
 *
 */

define(['oj/file', 'oj/javascript'], function(file, javscript) {
    
    
    var container = document.body,
        font      = {
            size  : '14',
            width : undefined,
            height: undefined
        },
        editel  = undefined,    // editarea element
        text    = 'X',          // holds text of editarea
        lines   = 0,
        command = {             // command bar elements
            el    : undefined,
            input : undefined,
            info  : undefined,
            mode  : false
        },
        ctrl = {                // command bar elements
            el    : undefined,
            saveFile: undefined,
            increaseFont: undefined,
            decreaseFont: undefined
        },
        underlay = {            // span of syntax highlighting
            el    : undefined,
            tmp   : undefined,
            els   : []
        },
        ruler = {               // span of ruler
            padding : 2,
            el    : undefined,
            tmp   : undefined,
            els   : []
        },
        cursor      = {         // cursor information
            el        : undefined,
            pos       : -1,
            indent    : undefined,
            offsetTop : 0
        },
        insertmode = true,        // what mode we are in
        editbuffer = '',          // buffer of keys pressed in edit mode
        tokens     = [],          // holds previously parsed tokens
        colours    = {
            token  : [            // syntax highlight colours
                '#999999',        //operator
                '#3a3a3a',        //space
                '#3a3a3a',        //linebreak
                '#524E58',        //comment
                '#C19859',        //cword
                '#FFFFFF',        //name
                '#0086FF',        //number
                '#7D8349',        //string
                '#555555',        //operator
                '#eeeeee'         //eof
            ],
            ruler       : '#222222',
            linenumber  : '#777777',
            lineborder  : '#555555',
            cmdbarbg    : '#eeeeee',
            background  : '#141414',
            cursorInsert: '#FFFFFF',
            cursorEdit  : '#AAAAAA'
        },
        editfile    = undefined,
        lastKeyType = 'keyup',
        a = 0, b = 0, c = 0,
        pos         = 0,
        precursor   = 0,
        indent      = 0,
        ntokens     = 0,
        i = 0, el = 0,
        ntext       = 0,
        token       = 0
    
    
    
    oj.element(document.head, 'style', {'type': 'text/css'}, [
        'body {',
        '    font-family: "Monaco", "Menlo", "Ubuntu Mono", "Droid Sans Mono", "Consolas", monospace;',
        '    font-size  : ' + font.size + 'pt;',
        '}',
        '.ji_cmd_bar {',
        '    position  : fixed;',
        '    background: ' + colours.cmdbarbg + ';',
        '    right     : 0px;',
        '    bottom    : 0px;',
        '    visibility: hidden;',
        '    z-index   : 3;',
        '}',
        '.ji_ctrl_bar {',
        '    position  : fixed;',
        '    right     : 50px;',
        '    top       : 0px;',
        '    z-index   : 3;',
        '}',
        '.ji_ctrl_bar span {',
        '  color: #dddddd;',
        '  border: 1px solid #aaaaaa;',
        '  margin-left: 3px;',
        '  padding-top: 5px;',
        '  padding-left: 8px;',
        '  padding-right: 8px;',
        '  box-shadow: 0 0 10px #000000;',
        '  -webkit-box-shadow: 0 0 10px #000000;',
        '  -moz-box-shadow: 0 0 10px #000000;',
        '  background: linear-gradient(top,',
        '    rgb(225, 225, 225) 0%,',
        '    rgb(175, 175, 175) 50%,',
        '    rgb(155, 155, 155) 50%,',
        '    rgb(99, 99, 99) 100%);',
        '  background: -webkit-linear-gradient(top,',
        '    rgb(225, 225, 225) 0%,',
        '    rgb(175, 175, 175) 50%,',
        '    rgb(155, 155, 155) 50%,',
        '    rgb(99, 99, 99) 100%);',
        '  background: -moz-linear-gradient(top,',
        '    rgb(225, 225, 225) 0%,',
        '    rgb(175, 175, 175) 50%,',
        '    rgb(155, 155, 155) 50%,',
        '    rgb(99, 99, 99) 100%);',
        '}',
        '.ji_ctrl_bar span:hover, span:focus {',
        '  box-shadow: 0 0 10px #e06c1f;',
        '  -webkit-box-shadow: 0 0 10px #e06c1f;',
        '  -moz-box-shadow: 0 0 10px #e06c1f;',
        '  color: #e06c1f;',
        '  transition: box-shadow 0.4s, -webkit-box-shadow 0.4s, -moz-box-shadow 0.4s, color 0.4s;',
        '  -webkit-transition: box-shadow 0.4s, -webkit-box-shadow 0.4s, -moz-box-shadow 0.4s, color 0.4s;',
        '  -moz-transition: box-shadow 0.4s, -webkit-box-shadow 0.4s, -moz-box-shadow 0.4s, color 0.4s;',
        '  -ms-transition: box-shadow 0.4s, -webkit-box-shadow 0.4s, -moz-box-shadow 0.4s, color 0.4s;',
        '  -o-transition: box-shadow 0.4s, -webkit-box-shadow 0.4s, -moz-box-shadow 0.4s, color 0.4s;',
        '  cursor: pointer;',
        '}',
        '.ji_cmd_input {',
        '    background : none;',
        '    border     : none;',
        '    font-family: "Monaco", "Menlo", "Ubuntu Mono", "Droid Sans Mono", "Consolas", monospace;',
        '    font-size  : ' + font.size + 'pt;',
        '}',
        '.ji_cmd_info {',
        '    white-space: pre;',
        '    padding    : 4px;',
        '}',
        '.ji_ruler {',
        '    white-space  : pre;',
        '    color        : ' + colours.linenumber + ';',
        '    text-align   : right;',
        '    float        : left;',
        '    padding-left : 5px;',
        '    padding-right: 5px;',
        '    background   : ' + colours.ruler + ';',
        '    width        : 2em;',
        '    border-right : 1px solid ' + colours.lineborder + ';',
        '}',
        '.ji_ruler_num {',
        '    white-space: pre;',
        '    color      : ' + colours.linenumber + ';',
        '}',
        '.ji_edit {',
        '    position  : absolute;',
        '    z-index   : 2;',
        '    top       : 0;',
        '    background: none;',
        '    color     : transparent;',
        '    border    : none;',
        '    outline   : none;',
        '    padding   : 0;',
        '    margin    : 0;',
        '    overflow  : hidden;',
        '    resize    : none;',
        '    font-family: "Monaco", "Menlo", "Ubuntu Mono", "Droid Sans Mono", "Consolas", monospace;',
        '    font-size  : ' + font.size + 'pt;',
        '}',
        '.ji_underlay {',
        '    white-space: pre;',
        '    position   : absolute;',
        '    z-index    : 1;',
        '}',
        '.ji_underlay_tile {',
        '    white-space: pre;',
        '}',
        '.ji_cursor {',
        '    white-space: pre;',
        '    position   : absolute;',
        '    z-index    : 3;',
        '    border-left: 2px solid ' + colours.cursorInsert + ';',
        '}'
    ])
    
    
    function open(path) {
        /*
         * Opens the File at path for editing.
         */
        
        editfile = file.open(path)
        
        editfile.read(function(read) {
            document.title = read.file.path
            editel.value = read.file.content
            editel.selectionStart = editel.selectionEnd = 0
            refreshDisplay()
        })
    }
    
    
    function save(callback) {
        /*
         * Writes contents of editarea to File and flushes to server.
         */
        
        if (typeof editfile === 'undefined')
            return
        
        editfile.content = editel.value;
        
        i = 0
        ntext = command.info.textContent
        
        editfile.flush(function(flush) {
            if (flush.hasOwnProperty('multiparts')) {
                a = flush.multiparts
                return
            }
            if (flush.hasOwnProperty('multipart')) {
                command.info.textContent = '' + ++i + '/' + a + '  ' + ntext
                return
            }
            if (flush.hasOwnProperty('success'))
                command.info.textContent = '-saved-  ' + ntext
            else if (flush.hasOwnProperty('error'))
                command.info.textContent = flush.error + '  ' + ntext
            
            if (typeof callback === 'function')
                callback(flush)
        })
    }
    
    
    function scrollToCursor() {
        /*
         * Scrolls view so cursor is visible.
         */
        
        i = (document.documentElement.scrollTop || document.body.scrollTop )
        precursor = window.innerHeight || document.documentElement.clientHeight
        
        if (cursor.offsetTop < i)
            i = cursor.offsetTop
        else if (cursor.offsetTop + font.height > i + precursor)
            i = (cursor.offsetTop + font.height) - precursor
        
        document.documentElement.scrollTop = i;
        document.body.scrollTop = i;
    }
    
    
    function refreshCursor() {
        /*
         * Updates cursor position.
         */
        
        precursor = editel.value.substring(0, editel.selectionStart).replace(/[^\n]*\n/g, '\n')
        indent = precursor.replace(/\n/g, '').length
        
        if (insertmode)
            cursor.indent = indent
        
        cursor.offsetTop = ruler.el.offsetTop + precursor.replace(/[^\n]/g, '').length * font.height
        cursor.pos = editel.selectionStart
        cursor.el.style.left = String(ruler.el.offsetWidth + ruler.padding + (indent * font.width)) + 'px'
        cursor.el.style.top = String(cursor.offsetTop) + 'px'
        
        scrollToCursor()
            
        command.info.textContent = cursor.pos + ':' + String(editel.value.length)
        
        if (typeof editfile === 'object')
            command.info.textContent += '  ' + editfile.path + '  '
        else
            command.info.textContent += '  cmd  '
    }
    
    
    function refreshDisplay() {
        /*
         * Updates display.
         */
        
        if (cursor.pos !== editel.selectionStart)
            refreshCursor()
        
        if (text === editel.value)
            return
        
        text = editel.value
        
        ntokens = javascript.tokens(text)
        
        // update the number of rows on the editarea and ruler count
        i = text.split('\n').length

        while (i > editel.rows)
            ruler.els[editel.rows++].style.display = 'inline'
        
        while (i <= lines)
            ruler.els[lines--].style.display = 'none'
        
        while (lines < i)
            ruler.els[lines++].style.display = 'inline'
        
        container.style.height = editel.offsetHeight
        
        
        // detach underlay from DOM while updating to avoid render updates
        container.removeChild(underlay.el)
        
        // update the syntax highlights
        for (i = 0; i < ntokens.length; i++) {
            token = ntokens[i]
            
            if (i < tokens.length && token.value === tokens[i].value)
                continue
            
            if (i === underlay.els.length) {
                el = underlay.tmp.cloneNode(true)
                underlay.els.push(el)
                underlay.el.appendChild(el)
            } else {
                el = underlay.els[i]
                el.style.display = 'inline'
            }
            
            el.style.color = colours.token[token.type]
            
            if (token.type === 2)
                el.textContent = token.value.replace(/\n/g, '$\n')
            else if (token.type === 1)
                el.textContent = token.value.replace(/ /g, '.')
            else
                el.textContent = token.value
        }
        
        // hide underlay elements that we aren't using
        while (i < underlay.els.length)
            underlay.els[i++].style.display = 'none'
        
        // re-attach underlay to DOM after tmp
        container.appendChild(underlay.el)
        
        // remember tokens
        tokens = ntokens.slice(0)
        
        editel.style.width = String((underlay.el.offsetWidth >= editel.clientWidth) ? underlay.el.offsetWidth : container.offsetWidth - 7 - ruler.offsetWidth) + 'px'
        editel.style.left = String(ruler.el.offsetWidth + ruler.padding) + 'px'
        underlay.el.style.top = editel.style.top
        underlay.el.style.left = editel.style.left
        
        scrollToCursor()
    }
    
    
    function editCount(key) {
        /*
         * Returns the buffered edit repeat count.
         */
        
        var re = new RegExp('.*?(\\d+)' + key + '$')
        return (re.test(editbuffer)) ? parseInt(editbuffer.replace(re, '$1'), 10) : 1
    };
    
    
    function insertMode(insert) {
        /*
         * Sets the mode to insert if true.
         */
        
        if (insert) {
            cursor.el.style.background = 'none'
            cursor.el.style.opacity = '1'
            command.el.style.visibility = 'hidden'
            insertmode = true
            editbuffer = ''
            return
        }
        
        cursor.el.style.background = colours.cursorEdit
        cursor.el.style.opacity = '.5'
        command.input.style.display = 'none'
        command.el.style.visibility = 'visible'
        insertmode = false
        editbuffer = ''
    }
    
    
    function commandInput(input) {
        /*
         * Sets the state of the command-line input.
         */
        
        if (input) {
            command.input.style.display = 'inline'
            command.input.value = ':'
            command.input.focus()
            command.input.selectionStart = command.input.selectionEnd = 1
            command.mode = true
            return
        }
        
        command.input.value = ''
        command.input.style.display = 'none'
        command.mode = false
        editel.focus()
    }
    
    
    function execute(cmd) {
        /*
         * Executes the specified command string.
         */
        
        var goto_re = /:([0-9]+|\$)/
        
        if (cmd === ':q') {
            window.open('', '_self', '')
            window.close()
        } else if (cmd === ':w') {
            save()
        } else if (cmd === ':wq') {
            save(function(m) {
                if (m.hasOwnProperty('success')) {
                    window.open('', '_self', '')
                    window.close()
                }
            })
        } else if (goto_re.test(cmd)) {
            var line = cmd.replace(goto_re, '$1')
            
            if (line === '$')
                line = text.split('\n').length
            else
                line = parseInt(line)
            
            var offset = 0
            
            while (offset != -1 && line > 1) {
                offset = text.indexOf('\n', offset) + 1
                line--
            }
            
            editel.selectionStart = editel.selectionEnd = offset
            refreshCursor()
        }
    }
    
    
    function cursorToLineBegin() {
        /*
         * Moves to the cursor the first none space character of the line.
         */
        
        var pos = text.lastIndexOf('\n', editel.selectionStart - 1) + 1
        
        while (text.charAt(pos) === ' ')
            pos++
        
        cursor.indent = pos - text.lastIndexOf('\n', pos)
        
        return pos
    }
    
    
    function cursorToLineEnd() {
        /*
         * Moves to the cursor the end of the line.
         */
        
        var pos = text.indexOf('\n', editel.selectionStart)
        
        if (pos === -1)
            pos = text.length
        
        cursor.indent = pos - text.lastIndexOf('\n', pos - 1) - 1
        
        return pos
    }
    
    
    function keyevent(event) {
        /*
         * Receives keydown and keyup events from the editarea.
         */
        
        c = event.keyCode
        pos = parseInt(editel.selectionStart, 10)
        
        if (insertmode) {
            if (c === 27)
                insertMode(false)
            else if (String(event.type) === 'keyup' || lastKeyType === 'keydown')
                refreshDisplay()
            
            lastKeyType = String(event.type)
            
            return true
        }
        
        if (String(event.type) === 'keyup')
            return false
        
        if (command.mode) {
            switch (c) {
                case 13: // ENTER execute command
                    execute(command.input.value)
                    commandInput(false)
                    return false
                case 27: // ESC exit command-line input
                    commandInput(false)
                    break
                default:
                    break
            }
            return true
        }
        
        switch (c) {
            case 52: // SHIFT+'4' ('$') cursor to end of line
                if (event.shiftKey)
                    pos = cursorToLineEnd()
                else
                    editbuffer += '4'
                break
            case 54: // SHIFT+'6' ('^') cursor to first nonespace of line
                if (event.shiftKey)
                    pos = cursorToLineBegin()
                else
                    editbuffer += '6'
                break
            case 59: // Firefox SHIFT+';' (':') enter command-line input
            case 186: // Chrome SHIFT+';' (':') enter command-line input
                if (event.shiftKey)
                    commandInput(true)
                return false
            case 65: // 'a' append
                pos = (event.shiftKey) ? cursorToLineEnd() : pos + 1
                insertMode(true)
                break
            case 73: // 'i' insert
                pos = (event.shiftKey) ? cursorToLineBegin() : pos
                insertMode(true)
                break
            case 37: // cursor left
            case 72: // 'h' cursor left
                if (pos > 0) {
                    pos--
                    cursor.indent--
                }
                editbuffer = ''
                break
            case 40: // cursor down
            case 74: // 'j' cursor down
                a = text.indexOf('\n', pos) + 1
                b = text.indexOf('\n', a)
                if (a !== 0) {
                    a += cursor.indent
                    pos = (a < b || b === -1) ? a : b
                }
                editbuffer = ''
                break
            case 38: // cursor up
            case 75: // 'k' cursor up
                if (pos - cursor.indent === 0)
                    return false
                a = text.lastIndexOf('\n', pos - 1)
                if (a !== -1) {
                    b = text.lastIndexOf('\n', a - 1) + 1
                    pos = (a - b < cursor.indent) ? a : b + cursor.indent
                }
                editbuffer = ''
                break
            case 39: // cursor right
            case 76: // 'l' cursor right
                if (pos < text.length) {
                    pos++
                    cursor.indent++
                }
                editbuffer = ''
                break
            case 68: // 'd' delete line if editbuffer ends with d
                if (/d$/g.test(editbuffer)) {
                    a = editCount('d')
                    while (a-- > 0) {
                        b = text.lastIndexOf('\n', pos - 1) + 1
                        c = text.indexOf('\n', pos) + 1
                        if (c === 0 && b > 0) b--
                        editel.value = text.slice(0, b)
                        if (c > 0)
                            editel.value += text.slice(c)
                        editel.selectionStart = editel.selectionEnd = b
                        refreshDisplay()
                    }
                    editbuffer = ''
                } else {
                    editbuffer += 'd'
                }
                break;
            case 79: // 'o' line append or line insert if SHIFT+
                editbuffer += 'o'
                a = editCount('o')
                while (a-- > 0) {
                    if (event.shiftKey) {
                        pos = text.lastIndexOf('\n', pos - 1) + 1
                    } else {
                        pos = text.indexOf('\n', pos) + 1
                        if (pos === 0) pos = text.length + 1
                    }
                    editel.value = text.slice(0, pos) + '\n' + text.slice(pos)
                    editel.selectionStart = editel.selectionEnd = pos
                    refreshDisplay()
                }
                insertMode(true)
                break
            case 88: // 'x' delete character if editbuffer ends with d
                editbuffer += 'x'
                a = editCount('x')
                while (a-- > 0) {
                    if (pos === text.length)
                        pos--
                    editel.value = text.slice(0, pos) + text.slice(pos + 1)
                    editel.selectionStart = editel.selectionEnd = pos
                    refreshDisplay()
                }
                editbuffer = ''
                break
            default:
                editbuffer += String.fromCharCode(c)
                break
        }
        
        editel.selectionStart = editel.selectionEnd = pos
        refreshCursor()
        return false
    }
    
    
    function updateFont() {
        document.body.style['font-size'] = font.size + 'pt'
        editel.style['font-size'] = font.size + 'pt'
        command.input['font-size'] = font.size + 'pt'
        var style = window.getComputedStyle(cursor.el, null)
        font.width = parseFloat(style.getPropertyValue('width'))
        font.height = parseFloat(style.getPropertyValue('height'))
        refreshCursor()
        text = ''
        refreshDisplay()
    }
    
    // control bar
    ctrl.el = oj.element(container, 'div', 'ji_ctrl_bar', [
        ctrl.saveFile = oj.element('span', ['&#x270e;']),
        ctrl.increaseFont = oj.element('span', ['A&#8314;']),
        ctrl.decreaseFont = oj.element('span', ['a&#8315;'])
    ])
    
    // command bar
    command.el = oj.element(container, 'div', 'ji_cmd_bar', [
        command.input = oj.element('input', 'ji_cmd_input', {'type':'text'}),
        command.info = oj.element('span', 'ji_cmd_info')
    ])
    
    ruler.el = oj.element('div', 'ji_ruler')
    ruler.tmp = oj.element('div', 'ji_ruler_num')
    editel = oj.element(container, 'textarea', 'ji_edit')
    underlay.el = oj.element(container, 'span', 'ji_underlay')
    underlay.tmp = oj.element('span', 'ji_underlay_tile')
    cursor.el = oj.element(container, 'span', 'ji_cursor', [' '])
    
    
    // allocate ruler number elements before attaching ruler to dom
    for (i = 0; i < 20000; i++) {
        el = ruler.el.appendChild(ruler.tmp.cloneNode(true))
        el.style.display = 'none'
        el.textContent = String(i + 1) + '\n'
        ruler.els.push(el)
    }
    
    // attach the ruler element
    container.appendChild(ruler.el)
    
    
    // container style
    container.style.position = 'relative'
    container.style.zIndex = '0'
    container.style.width = '100%'
    container.style.height = '100%'
    container.style.margin = '0'
    container.style.background = colours.background
    container.style.overflow = 'auto'
    document.onclick = container.onclick = function(){editel.focus()}
    
    // refresh the display when clicked or window resize
    editel.onclick = window.onresize = refreshDisplay
    
    ctrl.saveFile.onclick = function() {
        var chmode = insertmode
        if (chmode)
            insertMode(false)
        commandInput(true)
        command.input.value = ':w'
        save(function(m) {
            if (m.hasOwnProperty('success') && chmode)
                insertMode(true)
            commandInput(false)
        })
    }
    
    ctrl.increaseFont.onclick = function() {
        font.size++
        updateFont()
    }
    
    ctrl.decreaseFont.onclick = function() {
        font.size--
        updateFont()
    }
    
    // handle key events
    editel.onkeydown = editel.onkeyup = keyevent
    command.input.onkeydown = editel.onkeyup = keyevent
    
    updateFont()
    
    refreshDisplay()
    editel.focus()
    
    oj.args.forEach(function(arg){(arg[0]==='f' && open(arg[1]))})
    
})
